@miso.ai/doggoganger 0.9.0-beta.5 → 0.9.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js CHANGED
@@ -21,6 +21,10 @@ let { watch, ...argv } = yargs(hideBin(process.argv))
21
21
  describe: 'Watch files for changes',
22
22
  type: 'boolean',
23
23
  })
24
+ .option('answer-format', {
25
+ describe: 'Answer field format in answers API response',
26
+ type: 'string',
27
+ })
24
28
  .option('serve', {
25
29
  alias: 's',
26
30
  describe: 'Serve static files as well',
@@ -28,8 +32,8 @@ let { watch, ...argv } = yargs(hideBin(process.argv))
28
32
  })
29
33
  .argv;
30
34
 
31
- const { port, serve } = argv;
32
- argv = { port, serve };
35
+ const { port, serve, ['answer-format']: answerFormat } = argv;
36
+ argv = { port, serve, answerFormat };
33
37
 
34
38
  if (watch) {
35
39
  const exec = `node ${resolve(__dirname, 'server.js')} ${JSON.stringify(JSON.stringify(argv))}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miso.ai/doggoganger",
3
- "version": "0.9.0-beta.5",
3
+ "version": "0.9.0-beta.6",
4
4
  "description": "A dummy miso endpoint for demo and testing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,20 +1,27 @@
1
1
  import Router from '@koa/router';
2
2
  import { v4 as uuid } from 'uuid';
3
- import { lorem, articles, utils } from '../data/index.js';
3
+ import { lorem, md, articles, utils } from '../data/index.js';
4
4
 
5
- const router = new Router();
5
+ const { randomInt } = utils;
6
6
 
7
- const WPS = 20;
7
+ const CPS = 100;
8
8
  const ITEMS_LOADING_TIME = 3;
9
9
  const STAGES = [
10
10
  {
11
+ name: 'fetch',
11
12
  duration: 1.5,
12
- text: `Fetching results...`,
13
+ text: `Checking the question and fetching results...`,
13
14
  },
14
15
  {
16
+ name: 'verify',
17
+ duration: 1.5,
18
+ text: `Verifying results...`,
19
+ },
20
+ {
21
+ name: 'generate',
15
22
  duration: 1.5,
16
23
  text: `Generating answer...`,
17
- }
24
+ },
18
25
  ];
19
26
 
20
27
  function formatDatetime(timestamp) {
@@ -22,25 +29,34 @@ function formatDatetime(timestamp) {
22
29
  return str.endsWith('Z') ? str.slice(0, -1) : str;
23
30
  }
24
31
 
32
+ function answer(format) {
33
+ switch (format) {
34
+ case 'markdown':
35
+ return md.markdown({});
36
+ case 'plaintext':
37
+ default:
38
+ return lorem.lorem({
39
+ min: 50,
40
+ max: 100,
41
+ decorates: ['description'],
42
+ });
43
+ }
44
+ }
45
+
25
46
  const answers = new Map();
26
47
 
27
48
  class Answer {
28
49
 
29
- constructor(question, previous_question_id) {
50
+ constructor(question, previous_question_id, { answerFormat = 'plaintext' } = {}) {
30
51
  this.question_id = uuid();
31
52
  this.question = question;
32
53
  this.previous_answer_id = previous_question_id;
33
54
  this.timestamp = Date.now();
34
55
  this.datetime = formatDatetime(this.timestamp);
35
56
 
36
- this.answer = lorem.lorem({
37
- min: 50,
38
- max: 100,
39
- decorates: ['description'],
40
- output: 'array',
41
- });
42
- this.relatedResources = [...articles({ rows: utils.randomInt(6, 8) })];
43
- this.sources = [...articles({ rows: utils.randomInt(4, 6) })];
57
+ this.answer = answer(answerFormat);
58
+ this.relatedResources = [...articles({ rows: randomInt(6, 8) })];
59
+ this.sources = [...articles({ rows: randomInt(4, 6) })];
44
60
 
45
61
  this.previous_question_id = previous_question_id && answers.get(previous_question_id) || undefined;
46
62
  answers.set(this.question_id, this);
@@ -49,13 +65,14 @@ class Answer {
49
65
  get() {
50
66
  const now = Date.now();
51
67
  const elapsed = (now - this.timestamp) / 1000;
52
- const [answer, finished] = this._answer(elapsed);
68
+ const [stage, answer, finished] = this._answer(elapsed);
53
69
  const sources = this._sources(elapsed);
54
70
  const related_resources = this._relatedResources(elapsed);
55
71
  const { question_id, question, datetime, previous_question_id } = this;
56
72
 
57
73
  return {
58
74
  affiliation: undefined,
75
+ stage,
59
76
  answer,
60
77
  datetime,
61
78
  finished,
@@ -71,13 +88,13 @@ class Answer {
71
88
  for (const stage of STAGES) {
72
89
  elapsed -= stage.duration;
73
90
  if (elapsed < 0) {
74
- return [stage.text, false];
91
+ return [stage.name, stage.text, false];
75
92
  }
76
93
  }
77
- const wordCount = Math.floor(elapsed * WPS);
78
- const finished = wordCount >= this.answer.length;
79
- const text = (finished ? this.answer : this.answer.slice(0, wordCount)).join(' ');
80
- return [text, finished];
94
+ const length = Math.floor(elapsed * CPS);
95
+ const finished = length >= this.answer.length;
96
+ const text = finished ? this.answer : this.answer.slice(0, length);
97
+ return ['result', text, finished];
81
98
  }
82
99
 
83
100
  _sources(elapsed) {
@@ -96,22 +113,28 @@ class Answer {
96
113
 
97
114
  }
98
115
 
99
- router.post('/questions', (ctx) => {
100
- const { q: question, previous_answer_id } = JSON.parse(ctx.request.body);
101
- const answer = new Answer(question, previous_answer_id);
102
- const data = answer.get();
103
- ctx.body = JSON.stringify({ data });
104
- });
105
-
106
- router.get('/questions/:id/answer', (ctx) => {
107
- const { id } = ctx.params;
108
- const answer = answers.get(id);
109
- if (!answer) {
110
- ctx.status = 404;
111
- } else {
116
+ export default function({ answerFormat }) {
117
+ const options = Object.freeze({ answerFormat });
118
+ const router = new Router();
119
+
120
+ router.post('/questions', (ctx) => {
121
+ const { q: question, previous_answer_id } = JSON.parse(ctx.request.body);
122
+ const answerFormat = ctx.get('x-answer-format') || options.answerFormat;
123
+ const answer = new Answer(question, previous_answer_id, { answerFormat });
112
124
  const data = answer.get();
113
125
  ctx.body = JSON.stringify({ data });
114
- }
115
- });
126
+ });
127
+
128
+ router.get('/questions/:id/answer', (ctx) => {
129
+ const { id } = ctx.params;
130
+ const answer = answers.get(id);
131
+ if (!answer) {
132
+ ctx.status = 404;
133
+ } else {
134
+ const data = answer.get();
135
+ ctx.body = JSON.stringify({ data });
136
+ }
137
+ });
116
138
 
117
- export default router;
139
+ return router;
140
+ }
package/src/api/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Router from '@koa/router';
2
- import answers from './answers.js';
2
+ import _answers from './answers.js';
3
3
  import recommendation from './recommendation.js';
4
4
  import search from './search.js';
5
5
  import interactions from './interactions.js';
@@ -8,11 +8,13 @@ function use(router, path, middleware) {
8
8
  router.use(path, middleware.routes(), middleware.allowedMethods());
9
9
  }
10
10
 
11
- const router = new Router();
11
+ export default function(options) {
12
+ const router = new Router();
12
13
 
13
- use(router, '/answers', answers);
14
- use(router, '/recommendation', recommendation);
15
- use(router, '/search', search);
16
- use(router, '/interactions', interactions);
14
+ use(router, '/answers', _answers(options));
15
+ use(router, '/recommendation', recommendation);
16
+ use(router, '/search', search);
17
+ use(router, '/interactions', interactions);
17
18
 
18
- export default router;
19
+ return router;
20
+ };
@@ -1,5 +1,5 @@
1
1
  import * as u from './utils.js';
2
- import * as lorem from './lorem.js';
2
+ import * as fields from './fields.js';
3
3
 
4
4
  export function *articles({ rows, ...options } = {}) {
5
5
  for (let i = 0; i < rows; i++) {
@@ -9,39 +9,16 @@ export function *articles({ rows, ...options } = {}) {
9
9
 
10
10
  function article({} = {}) {
11
11
  const id = u.id();
12
- const prices = u.repeat(u.price, 1, 2);
13
- prices.sort();
14
- const seed = Math.floor(Math.random() * 1000);
15
12
 
16
13
  return {
17
14
  product_id: id,
18
- authors: lorem.lorem({
19
- min: 1,
20
- max: 3,
21
- fixedStarts: 0,
22
- decorates: ['title'],
23
- output: 'array',
24
- }),
15
+ authors: fields.authors(),
25
16
  categories: [],
26
- tags: lorem.lorem({
27
- min: 1,
28
- max: 4,
29
- fixedStarts: 0,
30
- output: 'array',
31
- }),
32
- title: lorem.lorem({
33
- min: 4,
34
- max: 10,
35
- fixedStarts: 0,
36
- decorates: ['title'],
37
- }),
38
- description: lorem.lorem({
39
- min: 20,
40
- max: 40,
41
- decorates: ['description'],
42
- }),
17
+ tags: fields.tags(),
18
+ title: fields.title({ size: [4, 10] }),
19
+ description: fields.description({ size: [20, 40] }),
43
20
  //html,
44
- cover_image: `https://picsum.photos/seed/${seed}/300`,
21
+ cover_image: fields.image(),
45
22
  url: `/products/${id}`,
46
23
  };
47
24
  }
@@ -0,0 +1,49 @@
1
+ import * as lorem from './lorem.js';
2
+ import { imageUrl } from './utils.js';
3
+
4
+ export function image({ size = 300 } = {}) {
5
+ return imageUrl(size);
6
+ }
7
+
8
+ export function authors({ size = [1, 3] } = {}) {
9
+ return lorem.lorem({
10
+ size,
11
+ decorates: ['title'],
12
+ output: 'array',
13
+ });
14
+ }
15
+
16
+ export function tags({ size = [1, 4] } = {}) {
17
+ return lorem.lorem({
18
+ size,
19
+ output: 'array',
20
+ });
21
+ }
22
+
23
+ // TODO: categories
24
+
25
+ export function title({ size = [2, 6] } = {}) {
26
+ return lorem.lorem({
27
+ size,
28
+ decorates: ['title'],
29
+ });
30
+ }
31
+
32
+ export function description({ size = [10, 20] } = {}) {
33
+ return lorem.lorem({
34
+ size,
35
+ decorates: ['description'],
36
+ });
37
+ }
38
+
39
+ export function availability() {
40
+ return Math.random() > 0.3 ? 'IN_STOCK' : 'OUT_OF_STOCK';
41
+ }
42
+
43
+ export function price() {
44
+ return Math.floor(Math.random() * 10000) / 100;
45
+ }
46
+
47
+ export function rating() {
48
+ return Math.floor(Math.random() * 500) / 100 + 1;
49
+ }
package/src/data/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * as md from './markdown/index.js';
1
2
  export * as lorem from './lorem.js';
2
3
  export * from './products.js';
3
4
  export * from './articles.js';
package/src/data/lorem.js CHANGED
@@ -7,8 +7,8 @@ import { randomInt } from './utils.js';
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const DEFAULT_WORDS = yaml.load(readFileSync(resolve(__dirname, './words.yaml'), 'utf8'));
9
9
 
10
- export function lorem({ decorates = [], min, max, n, output = 'string', ...options } = {}) {
11
- let iterator = limit({ min, max, n })(base(options));
10
+ export function lorem({ decorates = [], output = 'string', size, min, max, ...options } = {}) {
11
+ let iterator = limit(size || [min, max])(base(options));
12
12
  for (const decorate of decorates) {
13
13
  iterator = lookup(decorate)(iterator);
14
14
  }
@@ -20,14 +20,15 @@ const FNS = {
20
20
  array,
21
21
  title,
22
22
  description,
23
+ multiline,
23
24
  }
24
25
 
25
26
  function lookup(fn) {
26
- return typeof fn === 'string' ? FNS[fn]() : fn();
27
+ return typeof fn === 'string' ? FNS[fn]() : fn;
27
28
  }
28
29
 
29
30
  // base //
30
- export function *base({ words = DEFAULT_WORDS, fixedStarts = 2 } = {}) {
31
+ export function *base({ words = DEFAULT_WORDS, fixedStarts = 0 } = {}) {
31
32
  const wordsLength = words.length;
32
33
  for (let i = 0; ; i++) {
33
34
  yield words[i < fixedStarts ? i : Math.floor(Math.random() * wordsLength)];
@@ -43,9 +44,34 @@ export function array() {
43
44
  return iterator => [...iterator];
44
45
  }
45
46
 
47
+ export function multiline({
48
+ wordsPerLine = {
49
+ avg: 10,
50
+ std: 3,
51
+ min: 1,
52
+ },
53
+ } = {}) {
54
+ return iterator => {
55
+ let slen = gaussMS(wordsPerLine);
56
+ let result = '';
57
+ for (let word of iterator) {
58
+ if (result) {
59
+ if (slen-- === 0) {
60
+ result += '\n';
61
+ slen = gaussMS(wordsPerLine);
62
+ } else {
63
+ result += ' ';
64
+ }
65
+ }
66
+ result += word;
67
+ }
68
+ return result;
69
+ }
70
+ }
71
+
46
72
  // decorators //
47
- export function limit({ n, min = 5, max = 10 }) {
48
- n = n || randomInt(min, max);
73
+ export function limit(size = [5, 10]) {
74
+ const n = typeof size === 'number' ? size : randomInt(...size);
49
75
  return function *(iterator) {
50
76
  let i = 0;
51
77
  for (let word of iterator) {
@@ -66,8 +92,11 @@ export function title({} = {}) {
66
92
  }
67
93
 
68
94
  export function description({
69
- wordsPerSentenceAvg = 24,
70
- wordsPerSentenceStd = 5,
95
+ wordsPerSentence = {
96
+ avg: 24,
97
+ std: 5,
98
+ min: 1,
99
+ },
71
100
  } = {}) {
72
101
  return function *(iterator) {
73
102
  let word;
@@ -79,7 +108,7 @@ export function description({
79
108
  word = _word;
80
109
  if (slen === 0) {
81
110
  word = capitalize(word);
82
- slen = Math.max(1, gaussMS(wordsPerSentenceAvg, wordsPerSentenceStd));
111
+ slen = gaussMS(wordsPerSentence);
83
112
  }
84
113
  if (--slen === 0) {
85
114
  word += '.';
@@ -99,8 +128,23 @@ function capitalize(word) {
99
128
  return word[0].toUpperCase() + word.substring(1);
100
129
  }
101
130
 
102
- function gaussMS(mean, std) {
103
- return Math.round(gaussRandom() * std + mean);
131
+ // TODO: have a random variable expression
132
+ function gaussMS(args) {
133
+ if (typeof args === 'number') {
134
+ return Math.round(avg);
135
+ }
136
+ let { avg, std, min, max } = args;
137
+ if (std === undefined) {
138
+ std = avg / 4;
139
+ }
140
+ let n = gaussRandom() * std + avg;
141
+ if (min !== undefined) {
142
+ n = Math.max(min, n);
143
+ }
144
+ if (max !== undefined) {
145
+ n = Math.min(max, n);
146
+ }
147
+ return Math.round(n);
104
148
  }
105
149
 
106
150
  function gaussRandom() {
@@ -0,0 +1,233 @@
1
+ import { randomInt, imageUrl, shuffle } from '../utils.js';
2
+ import * as lorem from '../lorem.js';
3
+
4
+ // TODO: wild mode that generates edge cases
5
+
6
+ export function markdown({ features, blocks = [8, 12] } = {}) {
7
+ // TODO: block features
8
+ return [
9
+ atxHeading({ features }),
10
+ paragraph({ features }),
11
+ fencedCodeBlock({ features }),
12
+ paragraph({ features }),
13
+ table({ features }),
14
+ image(),
15
+ paragraph({ features }),
16
+ hr(),
17
+ atxHeading({ features }),
18
+ paragraph({ features }),
19
+ list({ features }),
20
+ paragraph({ features }),
21
+ ].join('\n\n');
22
+ }
23
+
24
+ // leaf blocks //
25
+ export function hr() {
26
+ // wild mode: while spaces at the beginning (< 4), in between, in the end
27
+ // wild mode: no line break for '*'
28
+ return '*-_'.charAt(randomInt(0, 2)).repeat(randomInt(3, 6));
29
+ }
30
+
31
+ export function atxHeading({ features, level = [1, 6], size = [1, 8], content }) {
32
+ const words = content || lorem.lorem({ size });
33
+ return `${'#'.repeat(randomInt(...level))} ${words}`;
34
+ }
35
+
36
+ export function setextHeading({ features, level = [1, 2], size = [1, 8], content }) {
37
+ const words = content || lorem.lorem({ size });
38
+ return `${words}\n${'=-'.charAt(randomInt(...level) - 1).repeat(3)}`;
39
+ }
40
+
41
+ export function linkReferenceDefinition({ label, destination, title }) {
42
+ return `[${label}]: ${destination}${title !== undefined ? ` ${title}` : ''}`;
43
+ }
44
+
45
+ export function indentedCodeBlock({ lang, content, size }) {
46
+ content = content || codeContent({ lang, size });
47
+ return indent(4, content);
48
+ }
49
+
50
+ export function fencedCodeBlock({ lang, content, size, fenceChar = '`' }) {
51
+ if (fenceChar === 'random') {
52
+ fenceChar = '`~'.charAt(randomInt(0, 1));
53
+ }
54
+ content = content || codeContent({ lang, size });
55
+ // TODO: escape fenceChar in content
56
+ return `${fenceChar.repeat(3)}${lang || ''}\n${content}\n${fenceChar.repeat(3)}`;
57
+ }
58
+
59
+ export function paragraph({ features, size = [20, 50] }) {
60
+ return lorem.lorem({ size, decorates: ['description', decorate({ features })] });
61
+ }
62
+
63
+ export function table({ features, columns = [2, 4], rows = [2, 8] }) {
64
+ columns = randomInt(...columns);
65
+ rows = randomInt(...rows);
66
+ const defs = [...multiply({ size: 1 }, columns - 1), { size: [3, 8] }];
67
+ const header = lorem.lorem({ size: columns, output: 'array' });
68
+ const delimiter = defs.map(() => '---');
69
+ const body = [ header, delimiter ];
70
+ for (let i = 0; i < rows - 1; i++) {
71
+ body.push(defs.map(({ size }) => lorem.lorem({ size })));
72
+ }
73
+ return body.map(tableRow).join('\n');
74
+ }
75
+
76
+ export function image({ url, imageSize = [400, 250], ...options } = {}) {
77
+ url = url || imageUrl(imageSize);
78
+ return `![${_content(options)}](${url})`;
79
+ }
80
+
81
+ // container blocks //
82
+ export function blockquote({ features, size = [3, 5] }) {
83
+ // TODO
84
+ return _blockquote(lorem.lorem({ size }));
85
+ }
86
+
87
+ const LIST_ITEM_TYPES = ['ordered', 'bullet', 'task'];
88
+
89
+ export function list({ features, type = 'random', count = [1, 8], size = [5, 15] }) {
90
+ count = typeof count === 'number' ? count : randomInt(...count);
91
+ const t = type === 'random' ? LIST_ITEM_TYPES[Math.floor(3 * Math.random())] : type;
92
+ const items = [];
93
+ while (count > 0) {
94
+ const c = randomInt(1, count);
95
+ let content = paragraph({ features, size });
96
+ if (c > 1) {
97
+ content += `\n${list({ features, type, count: c - 1, size })}`;
98
+ }
99
+ items.push(listItem(t, content));
100
+ count -= c;
101
+ }
102
+ return items.join('\n');
103
+ }
104
+
105
+ // inline //
106
+ export function codeSpan(options) {
107
+ return `\`${_content(options)}\``;
108
+ }
109
+
110
+ export function emphasis({ level = [1, 3], options }) {
111
+ level = typeof level === 'number' ? level : randomInt(...level);
112
+ const str = '_*'.charAt(randomInt(0, 1)).repeat(level);
113
+ return `${str}${_content(options)}${str}`;
114
+ }
115
+
116
+ export function link({ url = 'https://miso.ai', ...options } = {}) {
117
+ return `[${_content(options)}](${url})`;
118
+ }
119
+
120
+ // TODO: ref link
121
+ // TODO: autolink
122
+ // TODO: hard line break
123
+
124
+ const INLINE_FEATURES = {
125
+ 'code-span': () => ['`', '`'],
126
+ 'emphasis-1': () => multiply(_emphasisAdfix(1), 2),
127
+ 'emphasis-2': () => multiply(_emphasisAdfix(2), 2),
128
+ 'emphasis-3': () => multiply(_emphasisAdfix(3), 2),
129
+ 'link': ({ url = 'https://miso.ai' } = {}) => [`[`, `](${url})`],
130
+ };
131
+
132
+ function _emphasisAdfix(level = [1, 3]) {
133
+ level = typeof level === 'number' ? level : randomInt(...level);
134
+ return '_*'.charAt(randomInt(0, 1)).repeat(level);
135
+ }
136
+
137
+ const INLINE_FEATURE_LIST = Object.keys(INLINE_FEATURES);
138
+ const INLINE_FEATURE_SET = new Set(INLINE_FEATURE_LIST);
139
+
140
+ // decorator //
141
+ export function decorate({ features = INLINE_FEATURE_LIST, size = [1, 3], rest = [0, 8] } = {}) {
142
+ features = features.filter(f => INLINE_FEATURE_SET.has(f));
143
+
144
+ const unused = shuffle([...features]);
145
+ let unusedCursor = unused.length - 1;
146
+
147
+ const rollRest = () => typeof rest === 'number' ? rest : randomInt(...rest);
148
+ const rollFeatureSize = () => typeof size === 'number' ? size : randomInt(...size);
149
+ const rollFeatureType = () => unusedCursor >= 0 ? unused[unusedCursor--] : features[randomInt(0, features.length - 1)];
150
+
151
+ return function *(iterator) {
152
+ let count = rollRest();
153
+ let suffix;
154
+ let lastWord;
155
+ for (const word of iterator) {
156
+ if (lastWord) {
157
+ yield lastWord;
158
+ }
159
+ if (count === 0) {
160
+ if (suffix) {
161
+ lastWord = `${word}${suffix}`;
162
+ suffix = undefined;
163
+ count = rollRest();
164
+ } else {
165
+ const [prefix, s] = INLINE_FEATURES[rollFeatureType()]();
166
+ lastWord = `${prefix}${word}`;
167
+ suffix = s;
168
+ count = rollFeatureSize();
169
+ }
170
+ } else {
171
+ lastWord = word;
172
+ count--;
173
+ }
174
+ }
175
+ if (lastWord) {
176
+ yield suffix !== undefined ? `${lastWord}${suffix}` : lastWord;
177
+ }
178
+ };
179
+ }
180
+
181
+ // helper //
182
+ function _content({ size = [1, 3], content } = {}) {
183
+ return content || lorem.lorem({ size });
184
+ }
185
+
186
+ function _blockquote(content) {
187
+ return content.split('\n').map(line => `> ${line}`).join('\n');
188
+ }
189
+
190
+ export function indent(size, content) {
191
+ return content.split('\n').map(line => ' '.repeat(size) + line).join('\n');
192
+ }
193
+
194
+ export function listItem(type, content) {
195
+ const [firstLine, restLines] = content.split('\n', 2);
196
+ const result = `${listItemPrefix(type)} ${firstLine}`;
197
+ const indentSize = type === 'ordered' ? 3 : 2;
198
+ return !restLines ? result : result + `\n${indent(indentSize, restLines)}`;
199
+ }
200
+
201
+ function listItemPrefix(type, checked = 'random') {
202
+ switch (type) {
203
+ case 'ordered':
204
+ return '1.';
205
+ case 'bullet':
206
+ return '-';
207
+ case 'task':
208
+ const mark = (checked === 'random' ? Math.random() < 0.5 : !!checked) ? 'x' : ' ';
209
+ return `- [${mark}]`;
210
+ default:
211
+ throw new Error(`unknown list item type: ${type}`);
212
+ }
213
+ }
214
+
215
+ function codeContent({ lang, size = [10, 30] }) {
216
+ // TODO
217
+ switch (lang) {
218
+ default:
219
+ return lorem.lorem({ output: 'multiline', size });
220
+ }
221
+ }
222
+
223
+ function tableRow(cells) {
224
+ return `| ${cells.join(' | ')} |`;
225
+ }
226
+
227
+ function multiply(obj, i) {
228
+ const arr = [];
229
+ for (let j = 0; j < i; j++) {
230
+ arr.push(typeof obj === 'function' ? obj() : obj);
231
+ }
232
+ return arr;
233
+ }
@@ -1,5 +1,5 @@
1
1
  import * as u from './utils.js';
2
- import * as lorem from './lorem.js';
2
+ import * as fields from './fields.js';
3
3
 
4
4
  export function *products({ rows, ...options } = {}) {
5
5
  for (let i = 0; i < rows; i++) {
@@ -9,43 +9,22 @@ export function *products({ rows, ...options } = {}) {
9
9
 
10
10
  function product({} = {}) {
11
11
  const id = u.id();
12
- const prices = u.repeat(u.price, 1, 2);
12
+ const prices = u.repeat(fields.price, [1, 2]);
13
13
  prices.sort();
14
- const seed = Math.floor(Math.random() * 1000);
15
14
 
16
15
  return {
17
16
  product_id: id,
18
- authors: lorem.lorem({
19
- min: 1,
20
- max: 3,
21
- fixedStarts: 0,
22
- decorates: ['title'],
23
- output: 'array',
24
- }),
17
+ authors: fields.authors(),
25
18
  categories: [],
26
- tags: lorem.lorem({
27
- min: 1,
28
- max: 4,
29
- fixedStarts: 0,
30
- output: 'array',
31
- }),
32
- title: lorem.lorem({
33
- min: 2,
34
- max: 6,
35
- fixedStarts: 0,
36
- decorates: ['title'],
37
- }),
38
- description: lorem.lorem({
39
- min: 10,
40
- max: 20,
41
- decorates: ['description'],
42
- }),
19
+ tags: fields.tags(),
20
+ title: fields.title(),
21
+ description: fields.description(),
43
22
  //html,
44
- cover_image: `https://picsum.photos/seed/${seed}/300`,
23
+ cover_image: fields.image(),
45
24
  url: `/products/${id}`,
46
25
  sale_price: prices[0],
47
26
  original_price: prices[prices.length - 1],
48
- rating: u.rating(),
49
- availability: u.availability(),
27
+ rating: fields.rating(),
28
+ availability: fields.availability(),
50
29
  };
51
30
  }
package/src/data/utils.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export function randomInt(min, max) {
2
- return max > min ? min + Math.floor(Math.random() * (max - min)) : min;
2
+ return max == null || (max <= min) ? min : (min + Math.floor(Math.random() * (max - min)));
3
3
  }
4
4
 
5
- export function repeat(fn, min = 1, max = 2) {
6
- const n = randomInt(min, max);
5
+ // TODO: pass in size
6
+ export function repeat(fn, range) {
7
+ const n = randomInt(...range);
7
8
  const result = [];
8
9
  for (let i = 0; i < n; i++) {
9
10
  result.push(fn());
@@ -15,14 +16,16 @@ export function id() {
15
16
  return Math.random().toString(36).substring(2, 10);
16
17
  }
17
18
 
18
- export function availability() {
19
- return Math.random() > 0.3 ? 'IN_STOCK' : 'OUT_OF_STOCK';
20
- }
21
-
22
- export function price() {
23
- return Math.floor(Math.random() * 10000) / 100;
19
+ export function shuffle(array) {
20
+ for (let i = array.length - 1; i > 0; i--) {
21
+ const j = Math.floor(Math.random() * (i + 1));
22
+ [array[i], array[j]] = [array[j], array[i]];
23
+ }
24
+ return array;
24
25
  }
25
26
 
26
- export function rating() {
27
- return Math.floor(Math.random() * 500) / 100 + 1;
27
+ export function imageUrl(size) {
28
+ const seed = Math.floor(Math.random() * 1000);
29
+ const sizePath = Array.isArray(size) ? size.length > 1 ? `${size[0]}/${size[1]}` : `${size[0]}` : `${size}`;
30
+ return `https://picsum.photos/seed/${seed}/${sizePath}`;
28
31
  }
package/src/index.js CHANGED
@@ -3,12 +3,13 @@ import Router from '@koa/router';
3
3
  import cors from '@koa/cors';
4
4
  import serveStatic from 'koa-static';
5
5
  import { koaBody } from 'koa-body';
6
- import api from './api/index.js';
6
+ import _api from './api/index.js';
7
7
  import { exclusion } from './utils.js';
8
8
 
9
- export default function doggoganger({ port = 9901, serve = false } = {}) {
9
+ export default function doggoganger({ port = 9901, serve = false, ...options } = {}) {
10
10
  const app = new Koa();
11
11
  const router = new Router();
12
+ const api = _api(options);
12
13
 
13
14
  router.use('/api', api.routes(), api.allowedMethods());
14
15